﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System.Diagnostics;
using System;

namespace block_explorer
{
    class BaseEntity : IDrawableComponent
    {
        public Vector3 Position;
        public Vector3 Velocity;
        public Quaternion Orientation;
        public virtual Vector3 Target { get { return Position; } }

        protected float mYaw;
        protected float mPitch;

        public BoundingBox Bounds { get; set; }
        public BoundingBox WorldBounds { get { return new BoundingBox(Bounds.Min + Position, Bounds.Max + Position); } }
        public Scene Scene { get; set; }

        public bool Massless { get; set; }
        public bool Solid { get; set; }

        public bool DebugShowPosition { get; set; }
        public bool DebugShowTarget { get; set; }
        public bool DebugShowBounds { get; set; }

        private VertexBuffer mAxisBuffer;
        private VertexBuffer mBoundsBuffer;

        public BaseEntity()
        {
            Position = Vector3.Zero;
            Velocity = Vector3.Zero;
            Orientation = Quaternion.Identity;
        }

        public virtual void Initialize(GraphicsDevice device, ContentManager content)
        {
        }

        public virtual void Destroy()
        {
        }

        public virtual void Update(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            if (!Massless && Solid)
                Velocity += World.Gravity * dt;

            if (Solid)
                UpdateWithCollision(gameTime);
            else
                UpdateWithoutCollision(gameTime);
        }

        private void UpdateWithCollision(GameTime gameTime)
        {
            Debug.Assert(Bounds != null, "Need a bounding box set");
            Debug.Assert(Scene.Map != null, "Need a map to collide with");

            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            // while we're still moving and have time left in this step
            Vector3 currentVelocity = Velocity;
            while (currentVelocity.LengthSquared() > Single.Epsilon && dt > 0)
            {
                // calculate how far we've moved this step
                Vector3 positionAdjust = currentVelocity * dt;

                // create a bounding box that encompases the entire movement
                BoundingBox initialBox = WorldBounds;
                BoundingBox finalBox = new BoundingBox(initialBox.Min + positionAdjust, initialBox.Max + positionAdjust);
                BoundingBox allBox = Util.BoundingBoxEx.Contain(initialBox, finalBox);

                bool impacted = false;
                Vector3 impactCell = Vector3.Zero;
                float impactTime = Single.MaxValue;
                Vector3 impactNormal = Vector3.Zero;
                foreach (Vector3 cell in Scene.Map.GetCellsOnBox(allBox))
                {
                    // for each of the blocks involved in the movement collide against them
                    Block block = Scene.Map.GetBlockAt(cell);
                    if (block != null && block.IsSolid())
                    {
                        // found a solid block near our movement
                        // test to see if it collided
                        float tFirst;
                        float tLast;
                        Vector3 normal;
                        if (Scene.Map.IntersectMovingBlock(initialBox, currentVelocity, cell, out tFirst, out tLast, out normal))
                        {
                            // we actually collided
                            if (impactTime > tFirst)
                            {
                                // this impact was earlier than the previous impact
                                impacted = true;
                                impactTime = tFirst;
                                impactCell = cell;
                                impactNormal = normal;
                            }
                        }
                    }
                }

                if (impacted && impactTime < dt)
                {
                    // move to the collision point
                    Position += currentVelocity * impactTime;

                    Vector3 prevVelocity = currentVelocity;
                    if (impactNormal.LengthSquared() > 0)
                        currentVelocity += -(currentVelocity * impactNormal) * impactNormal;

                    dt -= impactTime;

                    // if this fails we're about to enter an endless loop.
                    Debug.Assert(impactTime > 0 || prevVelocity != currentVelocity);
                }
                else
                {
                    Position += currentVelocity * dt;
                    dt = 0;
                }
            }

            Velocity = currentVelocity;
        }

        private void UpdateWithoutCollision(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
            Position += Velocity * dt;
        }

        public virtual void Draw(GraphicsDevice device, Camera camera, Shaders.WorldShader effect)
        {
        }

        public virtual void Draw2D(GraphicsDevice device, Camera camera, Shaders.Render2DEffect effect)
        {
            if (DebugShowPosition)
                DrawPosition(device, camera, effect);

            if (DebugShowTarget)
                DrawTarget(device, camera, effect);

            if (DebugShowBounds)
                DrawBounds(device, camera, effect);
        }

        private void CreateAxisBuffer(GraphicsDevice device)
        {
            // buffer for the axis indicators
            mAxisBuffer = new VertexBuffer(device, VertexPositionColor.VertexDeclaration, 6, BufferUsage.WriteOnly);
            VertexPositionColor[] axisVerts = new VertexPositionColor[6];
            // x
            axisVerts[0] = new VertexPositionColor(new Vector3(0, 0, 0), Color.Red);
            axisVerts[1] = new VertexPositionColor(new Vector3(1, 0, 0), Color.Red);
            // y
            axisVerts[2] = new VertexPositionColor(new Vector3(0, 0, 0), Color.GreenYellow);
            axisVerts[3] = new VertexPositionColor(new Vector3(0, 1, 0), Color.GreenYellow);
            // z
            axisVerts[4] = new VertexPositionColor(new Vector3(0, 0, 0), Color.Blue);
            axisVerts[5] = new VertexPositionColor(new Vector3(0, 0, 1), Color.Blue);
            mAxisBuffer.SetData(axisVerts);
        }

        private void DrawPosition(GraphicsDevice device, Camera camera, Shaders.Render2DEffect effect)
        {
            if (mAxisBuffer == null)
            {
                CreateAxisBuffer(device);
            }

            effect.World = Matrix.CreateTranslation(Position);
            device.SetVertexBuffer(mAxisBuffer);

            foreach (EffectPass pass in effect.Passes)
            {
                pass.Apply();
                device.DrawPrimitives(PrimitiveType.LineList, 0, 3);
            }
        }

        private void DrawTarget(GraphicsDevice device, Camera camera, Shaders.Render2DEffect effect)
        {
            if (mAxisBuffer == null)
            {
                CreateAxisBuffer(device);
            }

            effect.World = Matrix.CreateTranslation(Target);
            device.SetVertexBuffer(mAxisBuffer);

            foreach (EffectPass pass in effect.Passes)
            {
                pass.Apply();
                device.DrawPrimitives(PrimitiveType.LineList, 0, 3);
            }
        }

        private void DrawBounds(GraphicsDevice device, Camera camera, Shaders.Render2DEffect effect)
        {
            Debug.Assert(Bounds != null, "Must have bounds set");

            if (mBoundsBuffer == null)
            {
                mBoundsBuffer = Util.BoundingBoxEx.CreateWireframe(device, Bounds, Color.White);
            }

            effect.World = Matrix.CreateTranslation(Position);
            device.SetVertexBuffer(mBoundsBuffer);

            foreach (EffectPass pass in effect.Passes)
            {
                pass.Apply();
                device.DrawPrimitives(PrimitiveType.LineList, 0, 12);
            }
        }

        public virtual void OnInputCommand(InputCommand command)
        {

        }

        public void Rotate(float headingDegrees, float pitchDegrees)
        {
            headingDegrees = -headingDegrees;
            pitchDegrees = -pitchDegrees;

            mPitch += pitchDegrees;

            if (mPitch > 90.0f)
            {
                pitchDegrees = 90.0f - (mPitch - pitchDegrees);
                mPitch = 90.0f;
            }

            if (mPitch < -90.0f)
            {
                pitchDegrees = -90.0f - (mPitch - pitchDegrees);
                mPitch = -90.0f;
            }

            mYaw += headingDegrees;

            if (mYaw > 360.0f)
                mYaw -= 360.0f;

            if (mYaw < -360.0f)
                mYaw += 360.0f;

            float heading = MathHelper.ToRadians(headingDegrees);
            float pitch = MathHelper.ToRadians(pitchDegrees);
            Quaternion rotation = Quaternion.Identity;

            // Rotate the camera about the world Y axis.
            if (heading != 0.0f)
            {
                Quaternion.CreateFromAxisAngle(ref World.Y_AXIS, heading, out rotation);
                Quaternion.Concatenate(ref rotation, ref Orientation, out Orientation);
            }

            // Rotate the camera about its local X axis.
            if (pitch != 0.0f)
            {
                Quaternion.CreateFromAxisAngle(ref World.X_AXIS, pitch, out rotation);
                Quaternion.Concatenate(ref Orientation, ref rotation, out Orientation);
            }
        }
    }
}
